ADR、RFC 与运行证据:决策记录保存当时为什么合理
一套订单查询服务要从单库扩分片:峰值 4,000 读/800 写、1.2TB、一年翻倍、6 人团队、跨账户审计必须保留——ADR 记录约束、选择和后果,而不是先选组件再补理由。本页定义 RFC/ADR/运行证据的分工、决策可逆性的真实边界,以及从提案到运行复查的完整样例。
文档各自解决不同问题
ADR 记录一项架构选择及其背景、后果和状态;RFC 用于较大设计提案和协作讨论。两者可以链接但职责不同。决策被替代时保留旧记录并引用新记录,不应悄悄改写历史结论。Michael Nygard 的原始 ADR 实践用上下文、决策、状态和后果保留推理,并建议保留被取代的记录;它是文档实践,不是规定系统应该使用哪种架构的标准。
运行证据包括负载测试、错误预算、故障注入、用户指标、成本和实际事故。实验要记录环境、输入和结果,避免把纸面推演说成已经运行验证。文档和图需要有维护人及更新触发条件;过期架构图会比缺图造成更大的误导风险。
RFC用于提出问题、候选与待解决争议;ADR记录已经作出的决定及原因;运行证据检验实施后是否仍满足当时假设。三者链接而非相互复制。ADR不是需求全集、源码讲义或长期任务清单,越长越不代表决策质量更高。普通可逆小修改不必制造新 ADR,是否写决定取决于未来理解成本和真实权衡,而非每次编辑次数。本仓库已有 docs/adr/ 与术语表,保持单一解释入口。
事实、假设、偏好必须分开
事实是已测得数据库存储读延迟、索引大小和锁等待;假设是明年流量翻倍;偏好是团队更熟悉 MySQL。把"分片能线性扩容"写成事实,会掩盖跨分片查询、热点租户和连接数等风险。没有测量时写"待验证",并给能改变结论的实验,不用"业界最佳实践"填空。
候选比较案例:方案 A 保留单写库,优化查询与归档,再建立异步读模型——安全能力每秒 1,200 写、8,000 读(教学输入),新增每月 8,000 元、迁移 6 人周;事务和审计边界保持,但单写能力有上限,读模型引入陈旧、重建和对账。方案 B 按账户分 8 片——均衡时每片 100 写/秒,但最大账户占 30% 流量,该片实际承载至少 240 写/秒,不能用平均值证明热点已解决;月成本增加 20,000 元、迁移 24 人周。粗算首年增量 A 为 156,000 元、B 为 480,000 元(人周按 10,000 元计),未含事故、长期运维、退出和机会成本,不能称为完整 TCO。
本例决策先选 A,并为写入逼近安全能力、审计查询无法满足、热点结构改变设置复查门槛。不是宣布永不分片,而是用可逆优化换取测量时间。若法律要求租户物理隔离,B 的某种隔离部署成为硬约束,成本高也不能被平均分淘汰。
可逆不是有一个开关
代码开关能把读取切回旧路径,但新系统已经接受的数据未必能被旧系统表示。把金额从整数分改成允许更细精度后,新值无法无损塞回旧字段;旧客户端不认识新增状态,也不能仅靠路由回滚。应逐项区分代码回退、流量回退、数据反向迁移与前向修复。
迁移状态可写成旧写唯一、兼容代码部署、双轨维护、回填追平、新读小流量、新读全量、旧写退出、旧结构退役。每次前进需要守卫:所有活跃写者支持新字段、差异核对通过、最大复制落后满足目标、回退窗口仍有效。双写一侧成功、另一侧失败时,必须有持久差异记录及重试责任;人工口头保证"会补上"不是恢复协议。
故障时序:t0 新读切入 5%;t1 新写生成旧格式无法表达的状态;t2 监控发现错误;t3 运维关闭新读开关。此时流量回退成功,旧应用却可能误解释 t1 的数据。真正停止条件应在 t1 前生效:先限制新语义写入,或者已经部署向后兼容读取;若做不到,承诺的是前向修复而非随时回滚。
决策记录与风险登记的具体产物
可以用以下 Markdown 内容提交一份 ADR,示例已经给出答案,不只是空模板:
决定:保留单写数据库,先做索引治理、归档与受控读投影。
依据:当前800写/秒,教学安全能力1200写/秒;未证明写扩展必需。
拒绝立即分片:跨账户审计、24人周迁移、热点并未被均衡假设解决。
接受后果:投影可短暂陈旧,主写瓶颈仍需持续测量。
复查:实际安全吞吐不足目标,或下一季预计峰值超过960写/秒。
阻塞风险:恢复实验未通过则不得切全量;数据库负责人提供证据。
退出:维持旧查询兼容窗口,超过窗口改为前向修复,不承诺盲回滚。960 是在题设安全能力上取 80% 的教学复查阈值,不是通用利用率定律。风险记录还要包括影响、检测、缓解、负责人和关闭条件。概率不知道时可以使用区间或定性分级,但不能随意编百分比算出精确收益;对低概率灾难性损失还应分析最坏情况和不可接受边界。
决定如何连接到实现
每个核心承诺找到接口/数据库约束、恢复任务、指标或实验。例"同键不会重复扣"指向唯一键、事务与请求摘要;例"30秒内恢复"指向发现/隔离/切换/验证时间线。测试名称、代码路径与环境版本比一张无上下文绿图更可复查。规范推导支持契约,固定tag源码支持该实现,运行结果支持该环境下的一次或一组观察;不能把某次PASS写成所有故障已穷尽。来源变化或依赖升级时,记录哪些证据失效。证据强度的分级规则见证据强度与结论范围。
一个从提案到运行复查的完整样例
教学RFC提出"峰值5000业务写/秒,通知允许5分钟送达,账务不能重复扣",列同步HTTP、Outbox、跨资源协调三个候选。未解决项是渠道结果未知的查询能力和relay故障后的清理速率。RFC阶段可以标待定,不应先写成系统已具备保证;业务负责人确认送达语义,数据负责人审查事务和唯一键,值班负责人确认恢复步骤。
通过后ADR只记录不可轻易逆转的选择:账务与发送意图同库事务,通知异步、至少一次投递、稳定事件ID与去重;后果包括中间态、补偿/对账、Outbox保留与容量成本。实现任务和全部参数不复制进ADR,分别链接接口契约、schema迁移、测试和运行手册。不可丢失的发送意图不等于渠道一定在5分钟完成,后者还依赖渠道与恢复能力。
上线证据分别记录:唯一键冲突测试、事务前后故障模型、恢复扫描覆盖、积压年龄与渠道错误。若relay停止10分钟,输入500/s则约产生30万待处理;恢复净多处理1000/s,理论约300秒清完,尚未计渠道配额。该结果说明原5分钟时效已受故障窗口影响,应决定时效排除项或提升恢复策略,不能把超时账单藏在平均延迟里。
后续业务将通知改成"实时支付确认",需新RFC/ADR讨论未知结果与不可逆副作用,原决定标已替代并保留历史。审阅者应能找到当前有效决定、前提改变点和旧方案为何当时合理,而不是只看到一份永远被覆盖的最新版文档。
ADR 的最小充分集与替代链
本页给出了 ADR 样例与可逆四类,评审时还需要明确"一份 ADR 最少写什么"以及"被替代时怎么留痕"。最小充分集五项:背景与约束、决定、后果(含接受的代价)、状态、复查条件;被替代时再加两项:替代者与替代原因。
替代链是 ADR 的历史结构:ADR-7 决定"单写库",一年后 ADR-12 决定"按账户分片"并声明 supersedes: ADR-7。旧记录保留原样,不修改、不删除;读者从新记录能跳到旧记录,理解"当时为什么合理"。反例:直接把 ADR-7 的内容改成"已分片",历史消失,未来的人无法知道分片决策的权衡过程。
ADR 不写实现细节与任务清单:实现细节放接口契约、迁移脚本与测试,任务清单放 issue 系统。ADR 越写越长不代表质量越高;判断标准是"一个没参与的人能否在十分钟内理解当时的约束、选择与代价"。
复查条件要可触发:例如"实际写入逼近安全能力""审计查询无法满足""租户物理隔离成为硬性要求"。没有复查条件的 ADR 等于宣布永久有效,这正是本页开头"决策记录保存当时为什么合理"的反面。
执行 python3 labs/review_models.py adr_minimal_set:预期输出替代链 ["ADR-7","ADR-12"] 与规则"保留旧记录、链接后继、不改历史"。脚本只检查结构,不评价决策本身。
两个合理选择:A 只在决策变化时重写一份最新 ADR,文档少但历史丢失;B 保留替代链,文档多但可追溯。本例选 B,并规定 ADR 的复查条件必须与所引用的证据台账(本主题 U74 页)同源。
架构师变化条件
把 A 安全写能力从 1,200 改为 850:当前 800 已接近边界,"可等待一年"的假设不再成立,需补压测、入口保护与短期容量方案并重开 ADR。法规要求物理隔离:可行集合改变,重新比较满足隔离的候选,成本不能平均掉硬约束。大账户占比从 30% 升到 70%:热点片至少 560 写/秒,需重新比较按业务子键拆分、专用租户与保留单点串行裁决。第四步模拟 t1 产生不可逆新数据并逐行写出旧服务怎样读取,不能解释则否决"开关保证回滚"。每次变化指明哪个新证据改变了哪条结论。
闭卷验收
本页 8 道闭卷题收进了下方自测面板:先回忆,再展开参考答案与评分点,最后给三档自评。